OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 248 LINES
 
[ HISTORY ]  [ UP ]
 

---
// Unified Players leaderboard page. Handles 4 combinations via URL/query:
//   - Season: season1, season2, all-time
//   - Sort:   ?sort=time (default) | ?sort=runs
//   - View:   /players/... (this page)
// The /characters/... route is the Character view counterpart and uses
// almost the same logic (only the view + class scope differ).
import PageLayout from "../../../../layouts/PageLayout.astro";
import LeaderboardTable from "../../../../components/Leaderboard/LeaderboardTable/
LeaderboardTable.astro";
import Pagination from "../../../../components/Leaderboard/Pagination/Pagination.a
stro";
import LeaderboardScopeFilter from "../../../../components/Leaderboard/Leaderboard
ScopeFilter/LeaderboardScopeFilter.astro";
import LeaderboardTypeNav from "../../../../components/Leaderboard/LeaderboardType
Nav/LeaderboardTypeNav.astro";
import LeaderboardHeader from "../../../../components/Leaderboard/LeaderboardHeade
r/LeaderboardHeader.astro";
import PlayerSearch from "../../../../components/PlayerSearch/PlayerSearch.astro";
import { fetchUnifiedLeaderboard } from "../../../../lib/api";
import { getEffectiveRealmSlug } from "../../../../lib/realms";

export const prerender = false;

const seasonParam = Astro.params.season || "season3";
const seasonMatch = seasonParam.match(/^season(\d+)$/);
const isAllTime = seasonParam === "all-time";
const currentSeason: number | "all-time" = isAllTime
  ? "all-time"
  : seasonMatch
    ? parseInt(seasonMatch[1], 10)
    : 3;

const scope = Astro.params.scope || "";
let scopeParts = scope.split("/").filter(Boolean);

// Sort lives at the front of the scope catchall as `by-runs` (default
// `by-time` is omitted).
let sortParam: "time" | "runs" = "time";
if (scopeParts[0] === "by-runs") {
  sortParam = "runs";
  scopeParts = scopeParts.slice(1);
}

// Old shape was /players/{sort}/... - strip and 301 to canonical.
if (
  scopeParts.length > 0 &&
  (scopeParts[0] === "overall-time" || scopeParts[0] === "total-runs")
) {
  const tail = scopeParts.slice(1).join("/");
  return Astro.redirect(
    `/challenge-mode/${seasonParam}/players/${tail || "global"}`,
    301,
  );
}

let regionParam = "global";
let realmParam = "";
let apiScope: "global" | "regional" | "realm" = "global";
if (scopeParts.length >= 1) {
  regionParam = scopeParts[0];
  if (regionParam !== "global") {
    apiScope = "regional";
    if (scopeParts.length >= 2 && scopeParts[1] && scopeParts[1] !== "all") {
      realmParam = scopeParts[1];
      apiScope = "realm";
    }
  }
}

// Connected-realm rollup: only parent realms have JSON files. If the user
// landed on a child realm slug (e.g. eu/garalon ? child of mirage-raceway),
// 301 to the parent so the URL matches the data we serve.
if (apiScope === "realm") {
  const parent = getEffectiveRealmSlug(regionParam, realmParam);
  if (parent && parent !== realmParam) {
    const tail = scopeParts.slice(2).join("/");
    const newPath =
      `/challenge-mode/${seasonParam}/players/${regionParam}/${parent}` +
      (tail ? `/${tail}` : "");
    const qs = Astro.url.searchParams.toString();
    return Astro.redirect(qs ? `${newPath}?${qs}` : newPath, 301);
  }
}

const pageParam = Astro.url.searchParams.get("page");
const currentPage = pageParam ? parseInt(pageParam, 10) : 1;

const seasonForNav: number = isAllTime ? 2 : (currentSeason as number);

let scopeLabel: string;
if (regionParam === "global") {
  scopeLabel = "Global";
} else if (realmParam) {
  scopeLabel = `${regionParam.toUpperCase()} - ${realmParam}`;
} else {
  scopeLabel = regionParam.toUpperCase();
}
const seasonLabel = isAllTime ? "All Time" : `Season ${currentSeason}`;

let data;
try {
  data = await fetchUnifiedLeaderboard(
    "player",
    sortParam,
    currentSeason,
    apiScope,
    {
      region: regionParam,
      realmSlug: realmParam || undefined,
      page: currentPage,
    },
    Astro.url.origin,
  );
} catch (error) {
  console.error("[Players page] Error:", error);
  throw error;
}

const pageTitle = `Player Rankings - ${scopeLabel} - ${seasonLabel}`;
const dungeonName =
  sortParam === "runs" ? "Player Rankings - Total Runs" : "Player Rankings";

// Build toggle URLs - sort lives in the path now (`by-runs` segment between
// view and scope), so toggling rebuilds the path. Other query params (e.g.
// from future filters) are preserved minus `page`.
const scopeTail = scopeParts.length ? scopeParts.join("/") : "global";
const otherParams = new URLSearchParams(Astro.url.searchParams);
otherParams.delete("page");
otherParams.delete("sort");
const otherQS = otherParams.toString();
const qsSuffix = otherQS ? `?${otherQS}` : "";

function buildPath(view: "players" | "characters", sort: "time" | "runs") {
  const sortSeg = sort === "runs" ? "/by-runs" : "";
  return `/challenge-mode/${seasonParam}/${view}${sortSeg}/${scopeTail}`;
}
const sortByTimeUrl = buildPath("players", "time") + qsSuffix;
const sortByRunsUrl = buildPath("players", "runs") + qsSuffix;
const viewPlayerUrl = buildPath("players", sortParam) + qsSuffix;
const viewCharacterUrl = buildPath("characters", sortParam) + qsSuffix;

// Determine which LeaderboardTable type to render.
const tableType = sortParam === "runs" ? "account-total-runs" : "account";
---

<PageLayout title={pageTitle}>
  <main class="leaderboard-page">
    <LeaderboardHeader
      season={seasonForNav}
      dungeonName={dungeonName}
      scopeLabel={`${seasonLabel} - ${scopeLabel}`}
    />
    <PlayerSearch />
    <LeaderboardTypeNav
      currentTab="player"
      currentRegion={regionParam}
      currentRealm={realmParam}
      currentSeason={seasonForNav}
    />

    <div class="cm-filters">
      <div class="filter-group">
        <label for="view-filter">View</label>
        <select id="view-filter">
          <option value="player" selected data-url={viewPlayerUrl}
            >Player</option
          >
          <option value="character" data-url={viewCharacterUrl}
            >Character</option
          >
        </select>
      </div>
      <div class="filter-group">
        <label for="sort-filter">Sort</label>
        <select id="sort-filter">
          <option
            value="time"
            selected={sortParam === "time"}
            data-url={sortByTimeUrl}
          >
            Time
          </option>
          <option
            value="runs"
            selected={sortParam === "runs"}
            data-url={sortByRunsUrl}
          >
            Total Runs
          </option>
        </select>
      </div>
      <LeaderboardScopeFilter
        currentRegion={regionParam}
        currentRealm={realmParam}
        currentSeason={isAllTime ? 0 : (currentSeason as number)}
        leaderboardType="player"
        currentSort={sortParam}
      />
    </div>

    <div id="leaderboard-content">
      <LeaderboardTable
        type={tableType}
        accounts={data.leaderboard}
        currentPage={data.pagination.currentPage}
        pageSize={data.pagination.pageSize}
        region={regionParam}
        realm={realmParam}
      />
    </div>

    <Pagination
      currentPage={data.pagination.currentPage}
      totalPages={data.pagination.totalPages}
      hasNextPage={data.pagination.hasNextPage}
      hasPrevPage={data.pagination.hasPrevPage}
      totalPlayers={data.pagination.totalAccounts ??
        data.pagination.totalPlayers ??
        data.pagination.totalRuns ??
        0}
      baseUrl={Astro.url.pathname}
    />
  </main>
</PageLayout>

<script>
  // Same data-url navigation pattern the other filters use.
  for (const id of ["view-filter", "sort-filter"]) {
    const el = document.getElementById(id) as HTMLSelectElement | null;
    if (!el) continue;
    el.addEventListener("change", (e) => {
      const target = e.target as HTMLSelectElement;
      const url = target.options[target.selectedIndex].dataset.url;
      if (url) window.location.href = url;
    });
  }
</script>

<style>
  .leaderboard-page {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }

  @media (max-width: 768px) {
    .leaderboard-page {
      padding: 15px;
    }
  }
</style>

---
// Unified Players leaderboard page. Handles
 4 combinations via URL/query:
//   - Season: season1, season2, all-time
//   - Sort:   ?sort=time (default) | ?sort=
runs
//   - View:   /players/... (this page)
// The /characters/... route is the Characte
r view counterpart and uses
// almost the same logic (only the view + cl
ass scope differ).
import PageLayout from "../../../../layouts/
PageLayout.astro";
import LeaderboardTable from "../../../../co
mponents/Leaderboard/LeaderboardTable/Leader
boardTable.astro";
import Pagination from "../../../../componen
ts/Leaderboard/Pagination/Pagination.astro";
import LeaderboardScopeFilter from "../../..
/../components/Leaderboard/LeaderboardScopeF
ilter/LeaderboardScopeFilter.astro";
import LeaderboardTypeNav from "../../../../
components/Leaderboard/LeaderboardTypeNav/Le
aderboardTypeNav.astro";
import LeaderboardHeader from "../../../../c
omponents/Leaderboard/LeaderboardHeader/Lead
erboardHeader.astro";
import PlayerSearch from "../../../../compon
ents/PlayerSearch/PlayerSearch.astro";
import { fetchUnifiedLeaderboard } from "../
../../../lib/api";
import { getEffectiveRealmSlug } from "../..
/../../lib/realms";

export const prerender = false;

const seasonParam = Astro.params.season || "
season3";
const seasonMatch = seasonParam.match(/^seas
on(\d+)$/);
const isAllTime = seasonParam === "all-time"
;
const currentSeason: number | "all-time" = i
sAllTime
  ? "all-time"
  : seasonMatch
    ? parseInt(seasonMatch[1], 10)
    : 3;

const scope = Astro.params.scope || "";
let scopeParts = scope.split("/").filter(Boo
lean);

// Sort lives at the front of the scope catc
hall as `by-runs` (default
// `by-time` is omitted).
let sortParam: "time" | "runs" = "time";
if (scopeParts[0] === "by-runs") {
  sortParam = "runs";
  scopeParts = scopeParts.slice(1);
}

// Old shape was /players/{sort}/... - strip
 and 301 to canonical.
if (
  scopeParts.length > 0 &&
  (scopeParts[0] === "overall-time" || scope
Parts[0] === "total-runs")
) {
  const tail = scopeParts.slice(1).join("/")
;
  return Astro.redirect(
    `/challenge-mode/${seasonParam}/players/
${tail || "global"}`,
    301,
  );
}

let regionParam = "global";
let realmParam = "";
let apiScope: "global" | "regional" | "realm
" = "global";
if (scopeParts.length >= 1) {
  regionParam = scopeParts[0];
  if (regionParam !== "global") {
    apiScope = "regional";
    if (scopeParts.length >= 2 && scopeParts
[1] && scopeParts[1] !== "all") {
      realmParam = scopeParts[1];
      apiScope = "realm";
    }
  }
}

// Connected-realm rollup: only parent realm
s have JSON files. If the user
// landed on a child realm slug (e.g. eu/gar
alon ? child of mirage-raceway),
// 301 to the parent so the URL matches the 
data we serve.
if (apiScope === "realm") {
  const parent = getEffectiveRealmSlug(regio
nParam, realmParam);
  if (parent && parent !== realmParam) {
    const tail = scopeParts.slice(2).join("/
");
    const newPath =
      `/challenge-mode/${seasonParam}/player
s/${regionParam}/${parent}` +
      (tail ? `/${tail}` : "");
    const qs = Astro.url.searchParams.toStri
ng();
    return Astro.redirect(qs ? `${newPath}?$
{qs}` : newPath, 301);
  }
}

const pageParam = Astro.url.searchParams.get
("page");
const currentPage = pageParam ? parseInt(pag
eParam, 10) : 1;

const seasonForNav: number = isAllTime ? 2 :
 (currentSeason as number);

let scopeLabel: string;
if (regionParam === "global") {
  scopeLabel = "Global";
} else if (realmParam) {
  scopeLabel = `${regionParam.toUpperCase()}
 - ${realmParam}`;
} else {
  scopeLabel = regionParam.toUpperCase();
}
const seasonLabel = isAllTime ? "All Time" :
 `Season ${currentSeason}`;

let data;
try {
  data = await fetchUnifiedLeaderboard(
    "player",
    sortParam,
    currentSeason,
    apiScope,
    {
      region: regionParam,
      realmSlug: realmParam || undefined,
      page: currentPage,
    },
    Astro.url.origin,
  );
} catch (error) {
  console.error("[Players page] Error:", err
or);
  throw error;
}

const pageTitle = `Player Rankings - ${scope
Label} - ${seasonLabel}`;
const dungeonName =
  sortParam === "runs" ? "Player Rankings - 
Total Runs" : "Player Rankings";

// Build toggle URLs - sort lives in the pat
h now (`by-runs` segment between
// view and scope), so toggling rebuilds the
 path. Other query params (e.g.
// from future filters) are preserved minus 
`page`.
const scopeTail = scopeParts.length ? scopeP
arts.join("/") : "global";
const otherParams = new URLSearchParams(Astr
o.url.searchParams);
otherParams.delete("page");
otherParams.delete("sort");
const otherQS = otherParams.toString();
const qsSuffix = otherQS ? `?${otherQS}` : "
";

function buildPath(view: "players" | "charac
ters", sort: "time" | "runs") {
  const sortSeg = sort === "runs" ? "/by-run
s" : "";
  return `/challenge-mode/${seasonParam}/${v
iew}${sortSeg}/${scopeTail}`;
}
const sortByTimeUrl = buildPath("players", "
time") + qsSuffix;
const sortByRunsUrl = buildPath("players", "
runs") + qsSuffix;
const viewPlayerUrl = buildPath("players", s
ortParam) + qsSuffix;
const viewCharacterUrl = buildPath("characte
rs", sortParam) + qsSuffix;

// Determine which LeaderboardTable type to 
render.
const tableType = sortParam === "runs" ? "ac
count-total-runs" : "account";
---

<PageLayout title={pageTitle}>
  <main class="leaderboard-page">
    <LeaderboardHeader
      season={seasonForNav}
      dungeonName={dungeonName}
      scopeLabel={`${seasonLabel} - ${scopeL
abel}`}
    />
    <PlayerSearch />
    <LeaderboardTypeNav
      currentTab="player"
      currentRegion={regionParam}
      currentRealm={realmParam}
      currentSeason={seasonForNav}
    />

    <div class="cm-filters">
      <div class="filter-group">
        <label for="view-filter">View</label
>
        <select id="view-filter">
          <option value="player" selected da
ta-url={viewPlayerUrl}
            >Player</option
          >
          <option value="character" data-url
={viewCharacterUrl}
            >Character</option
          >
        </select>
      </div>
      <div class="filter-group">
        <label for="sort-filter">Sort</label
>
        <select id="sort-filter">
          <option
            value="time"
            selected={sortParam === "time"}
            data-url={sortByTimeUrl}
          >
            Time
          </option>
          <option
            value="runs"
            selected={sortParam === "runs"}
            data-url={sortByRunsUrl}
          >
            Total Runs
          </option>
        </select>
      </div>
      <LeaderboardScopeFilter
        currentRegion={regionParam}
        currentRealm={realmParam}
        currentSeason={isAllTime ? 0 : (curr
entSeason as number)}
        leaderboardType="player"
        currentSort={sortParam}
      />
    </div>

    <div id="leaderboard-content">
      <LeaderboardTable
        type={tableType}
        accounts={data.leaderboard}
        currentPage={data.pagination.current
Page}
        pageSize={data.pagination.pageSize}
        region={regionParam}
        realm={realmParam}
      />
    </div>

    <Pagination
      currentPage={data.pagination.currentPa
ge}
      totalPages={data.pagination.totalPages
}
      hasNextPage={data.pagination.hasNextPa
ge}
      hasPrevPage={data.pagination.hasPrevPa
ge}
      totalPlayers={data.pagination.totalAcc
ounts ??
        data.pagination.totalPlayers ??
        data.pagination.totalRuns ??
        0}
      baseUrl={Astro.url.pathname}
    />
  </main>
</PageLayout>

<script>
  // Same data-url navigation pattern the ot
her filters use.
  for (const id of ["view-filter", "sort-fil
ter"]) {
    const el = document.getElementById(id) a
s HTMLSelectElement | null;
    if (!el) continue;
    el.addEventListener("change", (e) => {
      const target = e.target as HTMLSelectE
lement;
      const url = target.options[target.sele
ctedIndex].dataset.url;
      if (url) window.location.href = url;
    });
  }
</script>

<style>
  .leaderboard-page {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }

  @media (max-width: 768px) {
    .leaderboard-page {
      padding: 15px;
    }
  }
</style>
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET